﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using Argos.Activities.Tests;
using Argos.Data.Model;
using Argos.Diagnostics;
using Argos.Tracking;

namespace Argos.WorkflowServices
{
	/// <summary>
	/// An implementation of the <see cref="ITestExecutionService"/> that can be used
	/// as a workflow service in a process that is hosting Argos monitoring workflows.
	/// </summary>
	/// <remarks>
	/// Developers can extend this class to add custom functionality to test execution.
	/// </remarks>
	public class TestExecutionService : WorkflowRuntimeService , ITestExecutionService
	{

		#region #ctor: public TestExecutionServiceBase()

		/// <summary>
		/// Creates a new instance of the class.
		/// </summary>
		public TestExecutionService() { }

		#endregion


		
		#region public event EventHandler<TargetTestedEventArgs> TargetTested

		private EventHandler<TargetTestedEventArgs> TargetTestedHandler;

		/// <summary>
		/// Fired when a target has finished its test, either successfully, or not successfully.
		/// </summary>
		public event EventHandler<TargetTestedEventArgs> TargetTested
		{
			add { this.TargetTestedHandler += value; }
			remove { this.TargetTestedHandler -= value; }
		}

		#endregion

		#region public event EventHandler<TargetTestingEventArgs> TargetTesting

		private EventHandler<TargetTestingEventArgs> TargetTestingHandler;

		/// <summary>
		/// Fired when a target is being tested.
		/// </summary>
		public event EventHandler<TargetTestingEventArgs> TargetTesting
		{
			add { this.TargetTestingHandler += value; }
			remove { this.TargetTestingHandler -= value; }
		}

		#endregion



		#region private IStorageService StorageService { get; }

		private IStorageService _StorageService;

		private IStorageService StorageService
		{
			get
			{
				if (null == _StorageService)
				{
					_StorageService = this.Runtime.GetService<IStorageService>();
					if (null == _StorageService)
						throw new InvalidOperationException(string.Format("The current workflow runtime does not contain a service that implements interface '{0}' which is required by the current operation.", typeof(IStorageService)));
				}
				return _StorageService;
			}
		}

		#endregion



		#region public object RunTest(Guid targetId)

		/// <summary>
		/// Runs the test for the given target.
		/// </summary>
		/// <param name="targetId">The id of the target whose test to run.</param>
		/// <returns>Returns the state of the executed test.</returns>
		public object RunTest(Guid targetId)
		{
			object state = null;
			Test t = this.StorageService.GetTargetTest(targetId);
			if (null != t)
			{
				this.OnTargetTesting(new TargetTestingEventArgs(targetId));
				try
				{
					state = this.RunTest(t);
					this.OnTargetTested(new TargetTestedEventArgs(targetId, state));
					Tracer.TraceData(TraceEventType.Information, 0, "Target test completed.", Tracer.CreateData("TargetId", targetId), Tracer.CreateData("TestState", state));
				}
				catch (Exception ex)
				{
					this.OnTargetTested(new TargetTestedEventArgs(targetId, ex));
					throw ex;
				}
				this.StorageService.StoreTestState(targetId, state);
			}
			return state;
		}

		#endregion

		#region public object RunTest(Test test)

		/// <summary>
		/// Runs the given test.
		/// </summary>
		/// <param name="test">The test to run.</param>
		/// <returns>Returns the state of the executed test.</returns>
		public object RunTest(Test test)
		{
			return this.RunTests(new List<Test>(new Test[] { test }));
		}

		#endregion

		#region public object RunTests(IList<Test> tests)

		/// <summary>
		/// Runs all the given tests in parallel.
		/// </summary>
		/// <param name="tests">A list of tests to run in parallel.</param>
		/// <returns>Returns the states of all tests executed in the method.</returns>
		public object RunTests(IList<Test> tests)
		{
			if (null == tests)
			{
				return null;
			}

			TestActivityPropertyTrackingService testTrackingService = this.Runtime.GetService<TestActivityPropertyTrackingService>();
			if (null == testTrackingService)
			{
				throw new InvalidOperationException(string.Format("Cannot exeute tests because the workflow runtime does not contain an instance of the '{0}' service.", typeof(TestActivityPropertyTrackingService).FullName));
			}


			object sync = new object();

			// Contains the longest test timeout of all tests being executed.
			TimeSpan longestTimeout = TimeSpan.Zero;

			// A dictionary that contains the instance id for a test activity as key, and true or false
			// as value, depending on whether the test has completed or not. This is used to check for
			// test timeouts.
			Dictionary<Guid, bool> completedInstances = new Dictionary<Guid, bool>();

			// A list of all exceptions that were thrown by the tests.
			List<Exception> exceptionList = new List<Exception>();

			// A list of the states that were returned by the executed tests.
			List<object> testStates = new List<object>();

			Dictionary<Guid, object> testStatesDictionary = new Dictionary<Guid, object>();

			// A dictionary containing the wait handles for each test being executed.
			Dictionary<Guid, AutoResetEvent> WaitHandles = new Dictionary<Guid, AutoResetEvent>();


			testTrackingService.WorkflowPropertiesRemoving += delegate(object sender, WorkflowPropertiesRemovingEventArgs e)
			{
				if (WaitHandles.ContainsKey(e.InstanceId))
				{
					object state = testTrackingService.GetWorkflowPropertyValue(e.InstanceId, "TestState");
					if (null != state)
					{
						testStatesDictionary[e.InstanceId] = state;
					}
				}
			};

			#region EventHandler<WorkflowCompletedEventArgs> workflowCompleted = delegate(object sender, WorkflowCompletedEventArgs e)

			EventHandler<WorkflowCompletedEventArgs> workflowCompleted = delegate(object sender, WorkflowCompletedEventArgs e)
			{
				lock (sync)
				{
					if (completedInstances.ContainsKey(e.WorkflowInstance.InstanceId))
					{
						completedInstances[e.WorkflowInstance.InstanceId] = true;
					}

					if (WaitHandles.ContainsKey(e.WorkflowInstance.InstanceId))
					{
						WaitHandles[e.WorkflowInstance.InstanceId].Set();
					}
				}
			};

			#endregion

			#region EventHandler<WorkflowTerminatedEventArgs> workflowTerminated = delegate(object sender, WorkflowTerminatedEventArgs e)

			EventHandler<WorkflowTerminatedEventArgs> workflowTerminated = delegate(object sender, WorkflowTerminatedEventArgs e)
			{
				lock (sync)
				{
					if (completedInstances.ContainsKey(e.WorkflowInstance.InstanceId))
					{
						completedInstances[e.WorkflowInstance.InstanceId] = true;
					}

					if (WaitHandles.ContainsKey(e.WorkflowInstance.InstanceId))
					{
						Tracer.TraceData(TraceEventType.Warning, 0, "A test activity terminated with an exception.", Tracer.CreateData("TestInstanceId", e.WorkflowInstance.InstanceId), e.Exception);
						exceptionList.Add(e.Exception);
						WaitHandles[e.WorkflowInstance.InstanceId].Set();
					}
				}
			};

			#endregion
			

			this.Runtime.WorkflowCompleted += workflowCompleted;
			this.Runtime.WorkflowTerminated += workflowTerminated;

			try
			{
				foreach (Test test in tests)
				{
					if (null != test)
					{
						if (test.ExecutionTimeout > longestTimeout)
						{
							longestTimeout = test.ExecutionTimeout;
						}

						// Create an instance of the test and add a wait handle
						// that is used to wait for the test to execute.
						Dictionary<string, object> activityProps = new Dictionary<string, object>(test.ActivityProperties);
						activityProps["ExecutionTimeout"] = test.ExecutionTimeout;
						WorkflowInstance instance = this.Runtime.CreateWorkflow(test.ActivityType, activityProps);
						completedInstances.Add(instance.InstanceId, false);
						WaitHandles.Add(instance.InstanceId, new AutoResetEvent(false));

						instance.Start();
					}
				}

				// Wait for all tests to complete or time out. Using the longest test timeout
				// as timeout for all tests.
				WaitHandle.WaitAll(WaitHandles.Values.ToArray(), longestTimeout, true);

				// Read the test states for all tests.
				foreach (Guid g in WaitHandles.Keys)
				{
					if (testStatesDictionary.ContainsKey(g))
					{
						testStates.Add(testStatesDictionary[g]);
					}
					else
					{
						var state = testTrackingService.GetWorkflowPropertyValue(g, "TestState");
						if (null != state)
							testStates.Add(state);
					}

				}
			}
			finally
			{
				// Not interested in these events anymore.
				this.Runtime.WorkflowTerminated -= workflowTerminated;
				this.Runtime.WorkflowCompleted -= workflowCompleted;


				// Now we need to make sure that every test instance is aborted if it is already running.
				foreach (var item in completedInstances.Where(p => !p.Value))
				{
					WorkflowInstance runningInstance = this.Runtime.TryGetWorkflow(item.Key);
					if (null != runningInstance)
					{
						runningInstance.Abort();
					}
				}
			}


			// Examine how the test completed and throw exceptions if there were any test failures.
			if (exceptionList.Count == 1)
			{
				throw exceptionList[0];
			}

			if (exceptionList.Count > 1)
			{
				throw new TestFailedException("Several tests terminated with an exception.", exceptionList.ToArray());
			}

			if (completedInstances.Values.Count(b => !b) == 1)
			{
				throw new TestTimedOutException(string.Format("The test timed out. Timeout: '{0}'.", longestTimeout));
			}

			if (completedInstances.Values.Count(b => !b) > 1)
			{
				throw new TestTimedOutException(string.Format("Several tests timed out. Timeout: '{0}'.", longestTimeout));
			}



			// Return the test states.
			if (testStates.Count == 1)
			{
				return testStates[0];
			}
			else if (testStates.Count > 1)
			{
				return testStates.ToArray();
			}

			return null;
		}

		#endregion



		#region protected virtual void OnTargetTested(TargetTestedEventArgs e)

		/// <summary>
		/// Fires the <see cref="TargetTested"/> event.
		/// </summary>
		protected virtual void OnTargetTested(TargetTestedEventArgs e)
		{
			if (null != this.TargetTestedHandler)
			{
				this.TargetTestedHandler.Invoke(this, e);
			}
		}

		#endregion

		#region protected virtual void OnTargetTesting(TargetTestingEventArgs e)

		/// <summary>
		/// Fires the <see cref="TargetTesting"/> event.
		/// </summary>
		protected virtual void OnTargetTesting(TargetTestingEventArgs e)
		{
			if (null != this.TargetTestingHandler)
			{
				this.TargetTestingHandler.Invoke(this, e);
			}
		}

		#endregion

	}

}
